Main#82
Conversation
实现权限系统的向后兼容,当权限记录不存在时: 1. 在文件列表操作中合并旧用户文件数据 2. 在文件读写操作中检查文件作者并自动创建权限记录
refactor: 重构撤销/重做功能以支持批量操作 fix(undo): 修复撤销高亮效果并优化性能 feat(settings): 新增座位表元素显示控制选项 style: 调整分隔条样式和交互效果 perf(autosave): 优化自动保存机制减少冗余操作 fix(drag): 修复拖拽操作与撤销系统的同步问题 chore: 清理无用代码和重复样式 docs: 更新部分注释和提示文本
Reviewer's GuideRefines seat chart UI personalization, tag display settings, undo behavior, and dark theme colors; introduces a centralized settings dialog controller; improves autosave and save-sync flow; restructures seat operations to batch-based undo; and updates various overlays and components for consistent theming and better interaction patterns. Sequence diagram for tag display mode settings redirectionsequenceDiagram
actor User
participant TagSettingsDialog
participant SettingsDialogController as useSettingsDialog
participant UnifiedSettingsDialog
participant UIPanel
participant TagData as useTagData
User->>TagSettingsDialog: click goToDisplayModeSettings
TagSettingsDialog->>TagSettingsDialog: close()
TagSettingsDialog->>SettingsDialogController: openSettings('global','ui')
SettingsDialogController->>UnifiedSettingsDialog: visible true, initialTab global, initialCategory ui
UnifiedSettingsDialog->>TagData: read tagDisplayMode, showTagsInSeatChart
UnifiedSettingsDialog->>UIPanel: render UIPanel with settings.ui
UIPanel->>TagData: bind localTagDisplayMode via computed
User->>UIPanel: change tag display mode
UIPanel->>TagData: setTagDisplayMode(mode)
TagData-->>UIPanel: tagDisplayMode updated
UIPanel-->>UnifiedSettingsDialog: emit update:settings
UnifiedSettingsDialog-->>SettingsDialogController: closeSettings()
Sequence diagram for batch seat operations with undo snapshotssequenceDiagram
actor User
participant SeatChart
participant SeatChartStore as useSeatChart
participant Undo as useUndo
User->>SeatChart: multi_select_seats
User->>SeatChart: click_clear_selection
SeatChart->>Undo: createSnapshot() as beforeSnapshot
SeatChart->>SeatChartStore: clearSeat(seatId,false) loop selectedSeats
SeatChart->>Undo: createSnapshot() as afterSnapshot
SeatChart->>Undo: recordBatch(beforeSnapshot,afterSnapshot)
Note right of Undo: pushes command type batch
User->>SeatChart: press_ctrl_z
SeatChart->>Undo: canUndo.value?
Undo-->>SeatChart: true
SeatChart->>Undo: undo()
Undo->>SeatChartStore: apply_snapshot(previous_state)
Undo->>SeatChartStore: compute_affected_seats_from_snapshot
SeatChartStore-->>SeatChart: updated seats with undo_highlight
User->>SeatChart: press_ctrl_y
SeatChart->>Undo: canRedo.value?
Undo-->>SeatChart: true
SeatChart->>Undo: redo()
Undo->>SeatChartStore: apply_snapshot(next_state)
SeatChartStore-->>SeatChart: updated seats with undo_highlight
Sequence diagram for legacy workspace permission migrationsequenceDiagram
actor User
participant Client
participant WorkspaceAPI as workspace.php
participant DbUsers
participant DbFiles
participant DbPermissions
User->>Client: open_cloud_workspace_list
Client->>WorkspaceAPI: action=list, username
WorkspaceAPI->>DbPermissions: getUserAccessibleFiles(username)
WorkspaceAPI->>DbUsers: get_array(username_files) as legacyFileIds
WorkspaceAPI->>WorkspaceAPI: merge fileIds and legacyFileIds
loop each fileId
WorkspaceAPI->>DbFiles: get(fileId)
DbFiles-->>WorkspaceAPI: fileRaw
WorkspaceAPI->>WorkspaceAPI: parse fileRaw to fileData
WorkspaceAPI->>DbPermissions: get(perm_fileId_username)
alt no_permission_record
WorkspaceAPI->>WorkspaceAPI: check fileData.metadata.author
alt author_is_username
WorkspaceAPI->>DbPermissions: grantFilePermission(owner)
end
end
WorkspaceAPI-->>Client: include file metadata in list
end
User->>Client: open_workspace(fileId)
Client->>WorkspaceAPI: action=open, fileId, username
WorkspaceAPI->>DbFiles: get(fileId)
WorkspaceAPI->>WorkspaceAPI: parse fileData
WorkspaceAPI->>DbPermissions: get(perm_fileId_username)
alt no_permission_record
WorkspaceAPI->>WorkspaceAPI: check fileData.metadata.author
alt author_is_username
WorkspaceAPI->>DbPermissions: grantFilePermission(owner)
WorkspaceAPI-->>Client: respond success with data
else
WorkspaceAPI-->>Client: respond 403
end
else has_permission_record
WorkspaceAPI->>WorkspaceAPI: hasFilePermission(read)?
alt has_read
WorkspaceAPI-->>Client: respond success with data
else
WorkspaceAPI-->>Client: respond 403
end
end
Updated class diagram for settings, autosave, undo, and seat displayclassDiagram
class useGlobalSettings {
+Ref settings
+defaultSettings
+saveToLocalStorage()
+resetSettings()
+applyThemeColor(color)
+applyColorScheme(mode)
}
class UISettings {
+bool enableAnimations
+number defaultZoom
+bool showStudentName
+bool showStudentNumber
+bool largeNameMode
+bool largeNumberMode
}
class useSettingsDialog {
+Ref visible
+Ref initialTab
+Ref initialCategory
+openSettings(tab,category)
+closeSettings()
}
class UnifiedSettingsDialog {
+Ref activeTab
+Ref activeCategory
+handleSave()
+handleCancel()
+handleReset()
}
class TagSettingsDialog {
+Ref localShowTagsInSeatChart
+goToDisplayModeSettings()
+close()
}
class UIPanel {
+Computed localSettings
+Computed localTagDisplayMode
+Computed localShowTags
+Computed hasHiddenElement
+Computed canEnableLargeName
+Computed canEnableLargeNumber
}
class useTagData {
+Ref tags
+Ref showTagsInSeatChart
+Ref tagDisplayMode
+setShowTagsInSeatChart(value)
+setTagDisplayMode(mode)
}
class SeatItem {
+Computed showStudentName
+Computed showStudentNumber
+Computed hasHiddenElement
+Computed largeNameMode
+Computed largeNumberMode
}
class CandidateItem {
+Computed isSelected
+Computed showStudentName
+Computed showStudentNumber
+Computed hasHiddenElement
+Computed largeNameMode
+Computed largeNumberMode
+handleClick()
}
class useStudentData {
+Ref selectedStudentId
+selectStudent(id)
+clearSelection()
}
class useEditMode {
+Ref currentMode
+Enum EditMode
+setMode(mode)
+setFirstSelectedSeat(seatId)
+clearFirstSelectedSeat()
}
class useAutoSave {
+Ref isAutoSaveEnabled
+Ref lastSaveTime
+startAutoSave()
+stopAutoSave()
+performAutoSave()
+markSaved()
+getAutoSaveBackup()
+clearAutoSaveBackup()
}
class useUndo {
+Ref canUndo
+Ref canRedo
+undo()
+redo()
+createSnapshot()
+recordAssign(seatId,studentId,previousSeatId)
+recordBatch(beforeSnapshot,afterSnapshot)
}
class SeatChart {
+Computed selectedSeatsArray
+handleSelClear()
+handleSelShuffle()
+handleSelAssign()
+handleAssignStudent(seatId,studentId)
}
class StudentList {
+handleDrop(event)
}
class GlobalDropZone {
+handleDrop(event)
}
class CloudWorkspaceDialog {
+handleSave()
}
class SidebarPanel {
+handleSaveWorkspace()
}
useGlobalSettings --> UISettings : has ui
UnifiedSettingsDialog --> useGlobalSettings : uses
UnifiedSettingsDialog --> useSettingsDialog : uses
TagSettingsDialog --> useTagData : uses
TagSettingsDialog --> useSettingsDialog : goToDisplayModeSettings
UIPanel --> useTagData : binds displayMode
UIPanel --> useGlobalSettings : uses ui flags
SeatItem --> useGlobalSettings : reads settings.ui
CandidateItem --> useGlobalSettings : reads settings.ui
CandidateItem --> useStudentData : selection
CandidateItem --> useEditMode : mode_control
SeatChart --> useUndo : batch_operations
SeatChart --> useGlobalSettings : uses
StudentList --> useUndo : batch_clear
GlobalDropZone --> useSeatChart : clearSeat
CloudWorkspaceDialog --> useAutoSave : markSaved
SidebarPanel --> useAutoSave : markSaved
AppHeader --> useSettingsDialog : openSettings
UnifiedSettingsDialog <.. AppHeader : rendered_by
Flow diagram for autosave and explicit save coordinationflowchart LR
subgraph AutoSaveComposable
AS_useAutoSave[useAutoSave]
lastSavedJson[(lastSavedJson)]
end
App[App_root]
Sidebar[SidebarPanel_handleSaveWorkspace]
CloudDialog[CloudWorkspaceDialog_handleSave]
App --> AS_useAutoSave
AS_useAutoSave -->|startAutoSave_interval| AutoTimer[Auto_save_timer]
AutoTimer -->|timeout| PerformAutoSave[performAutoSave]
PerformAutoSave --> GetJson[getWorkspaceJson]
GetJson -->|json| CheckDiff{json == lastSavedJson?}
CheckDiff -->|yes| StopNoChange[skip_save]
CheckDiff -->|no| SaveLocal[save_backup_to_localStorage]
SaveLocal --> UpdateLast[update_lastSavedJson_and_lastSaveTime]
Sidebar -->|user_click_save_to_local| SaveWorkspace[saveWorkspace]
SaveWorkspace -->|success| SidebarMark[markSaved]
SidebarMark --> AS_useAutoSave
CloudDialog -->|user_click_save_to_cloud| CloudSave[save_workspace_to_cloud]
CloudSave -->|success| CloudMark[markSaved]
CloudMark --> AS_useAutoSave
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 4 issues, and left some high level feedback:
- The large-name/large-number enablement logic in UIPanel (
hasHiddenElementincludeslocalShowTags) is inconsistent with SeatItem/CandidateItem (wherehasHiddenElementonly considers name/number), so hiding only tags can allow enabling large text in settings but it will never apply in seat/candidate display—consider aligning the conditions or updating the hint/UX to match actual behavior. - In
useSettingsDialog,initialCategoryis initialized to'ui'butopenSettings()without arguments resets it to'sync'; this means opening the settings from the header will default to a different category than the initial value—clarify the intended default and remove the mismatch.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The large-name/large-number enablement logic in UIPanel (`hasHiddenElement` includes `localShowTags`) is inconsistent with SeatItem/CandidateItem (where `hasHiddenElement` only considers name/number), so hiding only tags can allow enabling large text in settings but it will never apply in seat/candidate display—consider aligning the conditions or updating the hint/UX to match actual behavior.
- In `useSettingsDialog`, `initialCategory` is initialized to `'ui'` but `openSettings()` without arguments resets it to `'sync'`; this means opening the settings from the header will default to a different category than the initial value—clarify the intended default and remove the mismatch.
## Individual Comments
### Comment 1
<location path="src/components/settings/panels/UIPanel.vue" line_range="748-749" />
<code_context>
const { defaultSettings, applyThemeColor, applyColorScheme } = useGlobalSettings()
+const { tagDisplayMode, setTagDisplayMode, showTagsInSeatChart, setShowTagsInSeatChart } = useTagData()
const localSettings = computed({
- get: () => props.settings,
+ get: () => props.settings || {},
set: (value) => emit('update:settings', value)
})
</code_context>
<issue_to_address>
**issue (bug_risk):** Defaulting `localSettings` to `{}` can drop existing UI settings and cause inconsistent state.
When `props.settings` is `null`/`undefined`, this getter will now emit and persist changes against an empty object, so fields like `showStudentName` / `showStudentNumber` may become `undefined` or overwritten with partial data. Consider defaulting to a fully-populated settings object (e.g. `defaultSettings.ui`) or enforcing that the parent always provides a non-null settings instance and keeping the original getter, to avoid saving incomplete or corrupted UI settings in early-init cases (e.g. dialog opened before settings are loaded).
</issue_to_address>
### Comment 2
<location path="src/components/seat/SeatItem.vue" line_range="109" />
<code_context>
+ set: (val) => setShowTagsInSeatChart(val)
+})
+
+const hasHiddenElement = computed(() => {
+ return !localSettings.value.showStudentName || !localSettings.value.showStudentNumber || !localShowTags.value
+})
</code_context>
<issue_to_address>
**issue (bug_risk):** Large-font mode logic in `SeatItem` ignores tag visibility, which can desync with the settings constraints.
In `UIPanel.vue`, `hasHiddenElement` correctly includes tag visibility via `localShowTags` / `showTagsInSeatChart`, but in `SeatItem.vue` and `CandidateItem.vue` it only checks `showStudentName` and `showStudentNumber`. This means hiding only tags keeps `hasHiddenElement` false in those components, so large-font modes never activate even though the settings allow them. Please either reuse the same `hasHiddenElement` rule (including tag visibility) in these components, or update the settings constraints so they align with the rendering logic.
</issue_to_address>
### Comment 3
<location path="src/composables/useSettingsDialog.ts" line_range="4-11" />
<code_context>
+import { ref } from 'vue'
+
+const visible = ref(false)
+const initialTab = ref('global')
+const initialCategory = ref('ui')
+
+export function useSettingsDialog() {
+ const openSettings = (tab?: string, category?: string) => {
+ if (tab) initialTab.value = tab
+ else initialTab.value = 'global'
+ if (category) initialCategory.value = category
+ else initialCategory.value = 'sync'
+ visible.value = true
</code_context>
<issue_to_address>
**issue (bug_risk):** The default `initialCategory` differs between the ref and `openSettings`, which can cause inconsistent initial tab state.
`initialCategory` is initialized as `'ui'`, but `openSettings` falls back to `'sync'` when `category` is undefined. Since `AppHeader` calls `openSettings()` with no args while `TagSettingsDialog` uses `'ui'` explicitly, the first open will show `'sync'` and direct navigation `'ui'`. If `'ui'` is the intended default, update the `openSettings` fallback to `'ui'` to keep behavior consistent.
</issue_to_address>
### Comment 4
<location path="src/components/ui/GlobalDropZone.vue" line_range="19-25" />
<code_context>
import { useAuth } from '@/composables/useAuth'
import { useUndo } from '@/composables/useUndo'
import { useCloudWorkspaceDialog } from '@/composables/useCloudWorkspaceDialog'
+import { useAutoSave } from '@/composables/useAutoSave'
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Clearing a seat via the global drop zone is no longer undoable, which may be a regression versus other clear operations.
Previously, dropping a seat into the global drop zone called `recordClear` before `clearSeat`, so the action was added to the undo history. With this change, the handler only calls `clearSeat`:
```js
if (data.type === 'seat' && data.seatId) {
clearSeat(data.seatId)
clearSelection()
success('已将学生移出座位')
}
```
Other clear operations now use `createSnapshot` + `recordBatch` to remain undoable. To keep behavior consistent, this path should likely use the same snapshot-based undo flow, unless there’s a deliberate reason for this removal to be non-undoable.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const localSettings = computed({ | ||
| get: () => props.settings, | ||
| get: () => props.settings || {}, |
There was a problem hiding this comment.
issue (bug_risk): Defaulting localSettings to {} can drop existing UI settings and cause inconsistent state.
When props.settings is null/undefined, this getter will now emit and persist changes against an empty object, so fields like showStudentName / showStudentNumber may become undefined or overwritten with partial data. Consider defaulting to a fully-populated settings object (e.g. defaultSettings.ui) or enforcing that the parent always provides a non-null settings instance and keeping the original getter, to avoid saving incomplete or corrupted UI settings in early-init cases (e.g. dialog opened before settings are loaded).
| const showStudentNumber = computed(() => settings.value.ui.showStudentNumber !== false) | ||
| const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value) | ||
| const largeNameMode = computed(() => showStudentName.value && hasHiddenElement.value && settings.value.ui.largeNameMode) | ||
| const largeNumberMode = computed(() => showStudentNumber.value && hasHiddenElement.value && settings.value.ui.largeNumberMode) |
There was a problem hiding this comment.
issue (bug_risk): Large-font mode logic in SeatItem ignores tag visibility, which can desync with the settings constraints.
In UIPanel.vue, hasHiddenElement correctly includes tag visibility via localShowTags / showTagsInSeatChart, but in SeatItem.vue and CandidateItem.vue it only checks showStudentName and showStudentNumber. This means hiding only tags keeps hasHiddenElement false in those components, so large-font modes never activate even though the settings allow them. Please either reuse the same hasHiddenElement rule (including tag visibility) in these components, or update the settings constraints so they align with the rendering logic.
| const initialTab = ref('global') | ||
| const initialCategory = ref('ui') | ||
|
|
||
| export function useSettingsDialog() { | ||
| const openSettings = (tab?: string, category?: string) => { | ||
| if (tab) initialTab.value = tab | ||
| else initialTab.value = 'global' | ||
| if (category) initialCategory.value = category |
There was a problem hiding this comment.
issue (bug_risk): The default initialCategory differs between the ref and openSettings, which can cause inconsistent initial tab state.
initialCategory is initialized as 'ui', but openSettings falls back to 'sync' when category is undefined. Since AppHeader calls openSettings() with no args while TagSettingsDialog uses 'ui' explicitly, the first open will show 'sync' and direct navigation 'ui'. If 'ui' is the intended default, update the openSettings fallback to 'ui' to keep behavior consistent.
| import { useUndo } from '@/composables/useUndo' | ||
| import { useLogger } from '@/composables/useLogger' | ||
|
|
||
| const { isDraggingFromSeat } = useDragState() | ||
| const { clearSeat, getStudentAtSeat } = useSeatChart() | ||
| const { clearSeat } = useSeatChart() | ||
| const { clearSelection, students } = useStudentData() | ||
| const { recordClear } = useUndo() |
There was a problem hiding this comment.
suggestion (bug_risk): Clearing a seat via the global drop zone is no longer undoable, which may be a regression versus other clear operations.
Previously, dropping a seat into the global drop zone called recordClear before clearSeat, so the action was added to the undo history. With this change, the handler only calls clearSeat:
if (data.type === 'seat' && data.seatId) {
clearSeat(data.seatId)
clearSelection()
success('已将学生移出座位')
}Other clear operations now use createSnapshot + recordBatch to remain undoable. To keep behavior consistent, this path should likely use the same snapshot-based undo flow, unless there’s a deliberate reason for this removal to be non-undoable.
There was a problem hiding this comment.
Pull request overview
This PR updates the settings/seat-chart UX by adding new UI preferences (name/number visibility + large-font modes), centralizing settings dialog state via a composable, improving autosave behavior, and refining undo batching/highlighting. It also includes broad styling updates to use theme variables more consistently, plus legacy-permission migration logic in the workspace API.
Changes:
- Add UI settings for tag display mode and seat-chart element visibility (name/number/tags + large-font modes), and wire them into seat/candidate rendering.
- Introduce a global
useSettingsDialog()state to open/close the unified settings dialog (including deep-linking to a specific tab/category). - Improve autosave to skip redundant writes and mark autosave state on successful manual saves; adjust undo batching/highlighting; update various overlays/buttons to theme variables; add legacy permission migration in
workspace.php.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/composables/useUndo.js | Adjusts affected-seat highlighting logic for wrapper snapshot commands. |
| src/composables/useSettingsDialog.ts | Adds shared settings-dialog visibility + initial tab/category state. |
| src/composables/useGlobalSettings.js | Extends default UI settings (name/number visibility + large-font modes). |
| src/composables/useAutoSave.js | Skips autosave when workspace JSON unchanged; adds markSaved(). |
| src/components/workspace/CloudWorkspaceDialog.vue | Calls markSaved() after successful cloud save; theme variable styling tweaks. |
| src/components/ui/ResizeDivider.vue | Redesigns divider visuals and adjusts sizing/hover/drag styles. |
| src/components/ui/LoadingSpinner.vue | Uses theme-based overlay background via color-mix(). |
| src/components/ui/GlobalDropZone.vue | Simplifies drop handling by relying on seat-chart undo recording. |
| src/components/student/TagStudentSelector.vue | Replaces hard-coded border color with theme variable. |
| src/components/student/TagSettingsDialog.vue | Removes inline tag-mode controls and redirects to unified UI settings. |
| src/components/student/TagManager.vue | Updates modal overlay background to theme variable. |
| src/components/student/StudentRosterDialog.vue | Updates modal overlay background to theme variable. |
| src/components/student/StudentListHeader.vue | Random-assign now records a single batch undo snapshot. |
| src/components/student/StudentList.vue | Batch seat-clears now use snapshot-based undo batching. |
| src/components/student/StudentItem.vue | Replaces white text with inverse theme variable for tag text. |
| src/components/student/StudentEditDialog.vue | Updates modal overlay background to theme variable. |
| src/components/student/CandidateItem.vue | Adds selection + name/number visibility + large-font rendering options. |
| src/components/student/BatchEditDialog.vue | Updates overlay + badge text color to theme variables. |
| src/components/settings/panels/UIPanel.vue | Adds tag display mode + element visibility toggles (and large-font enable rules). |
| src/components/settings/panels/SyncPanel.vue | Replaces white with inverse text variable for primary button. |
| src/components/settings/panels/AboutPanel.vue | Replaces white with inverse text variable for tech tags. |
| src/components/settings/UnifiedSettingsDialog.vue | Switches to useSettingsDialog() for visibility and initial tab/category. |
| src/components/seat/SeatItem.vue | Adds name/number visibility + large-font rendering; tweaks undo highlight animation. |
| src/components/seat/SeatChart.vue | Moves several multi-seat operations to snapshot-based batch undo recording. |
| src/components/rule/RuleList.vue | Replaces white with inverse text variable for delete-confirm state. |
| src/components/relation/SeatRuleEditor.vue | Theme variable updates for dialog surfaces/overlays and active text colors. |
| src/components/layout/SidebarPanel.vue | Calls markSaved() on successful local save; log styling + overlay theme updates. |
| src/components/layout/SeatConfigDialog.vue | Updates overlay background to theme variable. |
| src/components/layout/ExportPreview.vue | Replaces inline button colors with classes + theme variables; other theme tweaks. |
| src/components/layout/EditorPanel.vue | Removes bottom border styling (visual/layout adjustment). |
| src/components/layout/AppHeader.vue | Migrates unified settings open/close to useSettingsDialog() state. |
| src/components/docs/RuleUsageGuide.vue | Replaces hard-coded border color with theme variable. |
| src/components/auth/SyncSettingsDialog.vue | Updates overlay background to theme variable. |
| src/components/auth/LoginDialog.vue | Updates overlay background to theme variable. |
| src/assets/main.css | Tweaks dark-mode text/border variables and comment wording. |
| src/App.vue | Updates undo/redo hotkeys to use computed refs (canUndo.value / canRedo.value). |
| public/api/workspace.php | Merges legacy file lists + migrates permissions; updates read/write permission logic. |
Comments suppressed due to low confidence (1)
public/api/workspace.php:255
- In the
listaction, legacy$legacyFileIdsare merged into$fileIds, but each listed file is added to$listwithout verifying the current user hasreadpermission for that$fileId. If the legacy list contains stale/incorrect ids, this can leak file metadata (name/time/size/author). Add a per-file permission check in the loop (similar toload), and only include legacy ids when the user is the author or whenhasFilePermission(..., 'read')passes (optionally creating a migrated permission record when author matches).
// 从权限表获取文件
$fileIds = getUserAccessibleFiles($dbPermissions, $username);
// 兼容旧数据:同时从用户文件列表获取文件
$userFilesKey = sanitizeDbKey($username . '_files');
$legacyFileIds = $dbUsers->get_array($userFilesKey);
if ($legacyFileIds && is_array($legacyFileIds)) {
$fileIds = array_unique(array_merge($fileIds ?: [], $legacyFileIds));
}
$list = [];
if ($fileIds && is_array($fileIds)) {
foreach ($fileIds as $fileId) {
if (!isValidFileId($fileId)) continue;
$sanitizedFileId = sanitizeDbKey($fileId);
$fileRaw = $dbFiles->get($sanitizedFileId);
if (!$fileRaw) continue;
$fileData = json_decode($fileRaw, true);
if (!$fileData || !isset($fileData['metadata'])) continue;
// 兼容旧数据:自动迁移权限记录
$permKey = sanitizeDbKey("perm_{$fileId}_{$username}");
if ($dbPermissions->get($permKey) === null) {
// 检查文件作者是否是当前用户
if (isset($fileData['metadata']['author']) && $fileData['metadata']['author'] === $username) {
grantFilePermission($dbPermissions, $fileId, $username, 'owner');
}
}
$list[] = [
'fileId' => $fileId,
'metadata' => $fileData['metadata']
];
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const initialTab = ref('global') | ||
| const initialCategory = ref('ui') | ||
|
|
||
| export function useSettingsDialog() { | ||
| const openSettings = (tab?: string, category?: string) => { | ||
| if (tab) initialTab.value = tab | ||
| else initialTab.value = 'global' | ||
| if (category) initialCategory.value = category | ||
| else initialCategory.value = 'sync' | ||
| visible.value = true |
There was a problem hiding this comment.
initialCategory is initialized to 'ui' (line 5) but openSettings() falls back to 'sync' when no category is provided (line 12). This inconsistency makes the default dialog state dependent on call path and is easy to misread/extend incorrectly. Align the defaults by picking one source of truth (e.g., make both default to 'sync'), and consider narrowing tab/category to known ids to prevent invalid values being stored.
| case 'redo_wrapper': | ||
| case 'undo_wrapper': | ||
| if (command.snapshot) { | ||
| command.snapshot.forEach(s => affected.push(s.id)) | ||
| command.snapshot.forEach(s => { | ||
| const current = seats.value.find(seat => seat.id === s.id) | ||
| if (current && (current.studentId !== s.studentId || current.isEmpty !== s.isEmpty)) { | ||
| affected.push(s.id) | ||
| } | ||
| }) | ||
| } |
There was a problem hiding this comment.
undo() pushes { type: 'redo_wrapper', snapshot: beforeSnapshot } onto redoStack, but executeRedo() has no redo_wrapper case (it only handles undo_wrapper). As a result, calling redo() after undo() will not restore the snapshot and effectively becomes a no-op beyond stack bookkeeping. Fix by making the wrapper command types symmetrical (either handle redo_wrapper in executeRedo, or push an undo_wrapper from undo() / adjust the naming consistently).
|
|
||
| const showStudentName = computed(() => settings.value.ui.showStudentName !== false) | ||
| const showStudentNumber = computed(() => settings.value.ui.showStudentNumber !== false) | ||
| const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value) |
There was a problem hiding this comment.
hasHiddenElement only considers name/number visibility, but the UI settings hint says large-font modes can be enabled when any element is hidden (including tags). If a user hides tags but keeps both name & number visible, largeNameMode/largeNumberMode will never activate even if enabled. Include tag visibility (e.g., showTagsInSeatChart) in hasHiddenElement so the runtime behavior matches the settings logic.
| const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value) | |
| const hasHiddenElement = computed(() => | |
| !showStudentName.value || | |
| !showStudentNumber.value || | |
| !showTagsInSeatChart.value | |
| ) |
|
|
||
| const showStudentName = computed(() => settings.value.ui.showStudentName !== false) | ||
| const showStudentNumber = computed(() => settings.value.ui.showStudentNumber !== false) | ||
| const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value) |
There was a problem hiding this comment.
hasHiddenElement only checks name/number visibility, but the settings panel enables large-font modes when any element is hidden (name/number/tags). If tags are hidden while name & number remain visible, largeNameMode/largeNumberMode won’t activate even if enabled. Include showTagsInSeatChart in hasHiddenElement so seat rendering matches the settings behavior.
| const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value) | |
| const hasHiddenElement = computed(() => !showStudentName.value || !showStudentNumber.value || showTagsInSeatChart.value === false) |
| // 没有权限记录,检查文件作者 | ||
| $fileData = $fileRaw ? json_decode($fileRaw, true) : null; | ||
| if ($fileData && isset($fileData['metadata']['author']) && $fileData['metadata']['author'] === $username) { | ||
| // 自动迁移:创建权限记录 | ||
| grantFilePermission($dbPermissions, $fileId, $username, 'owner'); | ||
| } else { | ||
| respond(['success' => false, 'message' => '无权删除该文件'], 403); |
There was a problem hiding this comment.
In delete, when there is no permission record and $fileRaw is null (file already deleted), $fileData becomes null and the code returns 403 (lines 327-335). This prevents users from removing stale legacy entries, even though the later else branch is designed to handle the “file already removed” case. Consider short-circuiting: if $fileRaw === null, skip the author-based permission migration and proceed to remove the id from the user list / revoke permissions and return success.
| // 没有权限记录,检查文件作者 | |
| $fileData = $fileRaw ? json_decode($fileRaw, true) : null; | |
| if ($fileData && isset($fileData['metadata']['author']) && $fileData['metadata']['author'] === $username) { | |
| // 自动迁移:创建权限记录 | |
| grantFilePermission($dbPermissions, $fileId, $username, 'owner'); | |
| } else { | |
| respond(['success' => false, 'message' => '无权删除该文件'], 403); | |
| // 没有权限记录时,仅在文件仍存在时回退检查文件作者; | |
| // 如果文件已不存在,则继续走后续清理逻辑,允许移除陈旧引用。 | |
| if ($fileRaw !== null) { | |
| $fileData = json_decode($fileRaw, true); | |
| if ($fileData && isset($fileData['metadata']['author']) && $fileData['metadata']['author'] === $username) { | |
| // 自动迁移:创建权限记录 | |
| grantFilePermission($dbPermissions, $fileId, $username, 'owner'); | |
| } else { | |
| respond(['success' => false, 'message' => '无权删除该文件'], 403); | |
| } |
Summary by Sourcery
Improve seat chart UI controls, tag display options, undo behavior, and workspace permission compatibility while refining theming and dialogs across the app.
New Features:
Bug Fixes:
Enhancements: